开发环境:
MDK:Keil 5.30
开发板:GD32F207I-EVAL
MCU:GD32F207IK
10.1串口简介
USART(Universal Synchronous Asynchronous Receiver and Transmitter,通用同步-异步接收发射器)提供了一种灵活的方法与使用工业标准NRZ异步串行数据格式的外部设备之间进行全双工数据交换。USART利用分数波特率发生器提供宽范围的波特率选择。它支持同步单向通信和半双工单线通信,也支持LIN(局部互连网),智能卡协议和IrDA(红外数据组织)SIR ENDEC规范,以及调制解调器(CTS/RTS)操作。它还允许多处理器通信。使用多缓冲器配置的DMA方式,可以实现高速数据通信。
虽然USART既可以同步又可以异步,但是常见的最常用的就是使用功能的异步功能,如果作为异步通信就是UART(Universal Asynchronous Receiver and Transmitter),可以说,UART是USART的子集,但是同步通信相比异步通信多了一根时钟同步信号线。
下面简单介绍下同步和异步。
在同步通讯中,收发设备双方会使用一根信号线表示时钟信号,在时钟信号的驱动下双方进行协调,同步数据,见下图。通讯中通常双方会统一规定在时钟信号的上升沿或下降沿对数据线进行采样。
在异步通讯中不使用时钟信号进行数据同步,它们直接在数据信号中穿插一些同步用的信号位,或者把主体数据进行打包,以数据帧的格式传输数据,见下图,某些通讯中还需要双方约定数据的传输速率,以便更好地同步。
在同步通讯中,数据信号所传输的内容绝大部分就是有效数据,而异步通讯中会包含有帧的各种标识符,所以同步通讯的效率更高,但是同步通讯双方的时钟允许误差较小,而异步通讯双方的时钟允许误差较大。
从上面的介绍可以看出,USART以同步方式通信需要时钟同步信号,但不需要额外的起始、停止位,可以实现更快的传输速度。但USART控制起来更复杂,因此本文主要讲解以异步通信。
异步串行通信以字符为单位,即一个字符一个字符地传送 。
串口外设的架构图看起来十分复杂,实际上对于软件开发人员来说,我们只需要大概了解串口发送的过程即可。从下至上,我们看到串口外设主要由三个部分组成,分别是波特率控制、收发控制和数据存储转移。
波特率控制
波特率,即每秒传输的二进制位数,用b/s(bps)表示,通过对时钟的控制可以改变波特率。在配置波特率时,我们向波特比率寄存器 USART_BAUD写入参数,修改了串口时钟的分频值USARTDIV。USART_BAUD寄存器包括两部分,分别是INTDIV(USARTDIV 的整数部分)和FRADIV(USARTDIV 的小数)部分,最终,计算公式为 USARTDIV= INTDIV+(FRADIV/16)。
USARTDIV 是对串口外设的时钟源进行分频的,USART0/5的系统时钟为PCLK2, USART1/2和UART3/4/6/7的系统时钟为PCLK1,串口的时钟源经过 USARTDIV 分频后分别输出作为发送器时钟及接收器时钟,控制发送和接收的时序。在使能USART之前,必须在时钟控制单元使能系统时钟。
收发控制
围绕着发送器和接收器控制部分,有好多个寄存器 :STAT0、USART_CTL0、USART_CTL1、USART_CTL2和 STAT1,即USART 的三个控制寄存器(Control Register)及一个状态寄存器(Status Register)。通过向寄存器写入 各种控制参数来控制发送和接收,如奇偶校验位、停止位等,还包括对USART 中断的控制;串口的状态在任何时候都可以从状态寄存器中查询得到。其中停止位的配置如下图所示。
发送配置步骤:
1.在USART_CTL0寄存器中置位UEN位,使能USART;
2.通过USART_CTL0寄存器的WL设置字长;
3.在USART_CTL1寄存器中写STB[1:0]位来设置停止位的长度;
4.如果选择了多级缓存通信方式,应该在USART_CTL2寄存器中使能DMA (DENT位);
5.在USART_BAUD寄存器中设置波特率;
6.在USART_CTL0寄存器中设置TEN位;
7.等待TBE置位;
8.向USART_DATA寄存器写数据;
9.若DMA未使能,每发送一个字节都需重复步骤7-8;
10.等待TC=1,发送完成。
在禁用USART或进入低功耗状态之前,必须等待TC置位。先读USART_STAT0然后再写USART_DATA可将TC位清0。在多级缓存通信方式(DENT=1)下,直接向TC写0,也能清TC。
接收配置步骤:
1.写USART_CTL0寄存器的WL位去设置字长;
2.在USART_CTL1寄存器中写STB[1:0]位来设置停止位的长度;
3.如果选择了多级缓存通信方式,应该在USART_CTL2寄存器中使能DMA(DENR位);
4.在USART_BAUD寄存器中设置波特率;
5.在USART_CTL0寄存器中置位UEN位,使能USART;
6.在USART_CTL0中设置REN位。
接收器在使能后若检测到一个有效的起始脉冲便开始接收码流。在接收一个数据帧的过程中会检测噪声错误,奇偶校验错误,帧错误和过载错误。
当接收到一个数据帧, USART_STAT0寄存器中的RBNE置位,如果设置了USART_CTL0寄存器中相应的中断使能位RBNEIE,将会产生中断。在USART_STAT0寄存器中可以观察接收状态标志。
软件可以通过读USART_DATA寄存器或者DMA方式获取接收到的数据。不管是直接读寄存器还是通过DMA,只要是对USART_DATA寄存器的一个读操作都可以清除RBNE位。
在接收过程中,需使能REN位,不然当前的数据帧将会丢失。
以上对串口通信进行了简单介绍,为了方便各位读者朋友更好的理解,在这里笔者将引入一个新的思想–系统分层思想。既然各位对着有意于嵌入式,那么必须得有对整个系统的架构要有一定的认知。对GD32裸机开发,我们可以将分为三层:物理层、协议层和应用层。前文讲了这么多也是对串口协议进行分析,常用的物理层的串口通信标准有232和485。
【注】UART和USART的区别
USART(universal synchronous asynchronous receiver and transmitte): 通用同步异步收发器,USART是一个串行通信设备,可以灵活地与外部设备进行全双工数据交换。
UART(universal asynchronous receiver and transmitter): 通用异步收发器,异步串行通信口(UART)就是我们在嵌入式中常说的串口,它还是一种通用的数据通信议。从名字上可以看出,USART在UART基础上增加了同步功能,即USART是UART的增强型。
当我们使用USART在异步通信的时候,它与UART没有什么区别,但是用在同步通信的时候,区别就很明显了:大家都知道同步通信需要时钟来触发数据传输,也就是说USART相对UART的区别之一就是能提供主动时钟。如GD32的USART可以提供时钟支持ISO7816的智能卡接口。
USART是指单片机的一个端口模块,可以根据需要配置成同步模式(SPI,I2C),也可以将其配置为异步模式,后者就是UART。所以说UART姑且可以称之为一个与SPI,I2C对等的“协议”,而USART则不是一个协议,而是更应该理解为一个实体。相比于同步通讯,UART不需要统一的时钟线,接线更加方便。但是,为了正常的对信号进行解码,使用UART通讯的双方必须事先约定好波特率,即每个码元的长度。
关于串口的深入理解,请参看笔者文章:
10.2串口通信的寄存器描述
串口常用的寄存器有状态寄存器(USART_STATx)、数据寄存器(USART_DATA)、波特比率寄存器(USART_BAUD)、控制寄存器 (USART_CTLx)。
10.3串口硬件
串口的接口通过三个引脚与其他设备连接在一起。任何USART双向通信至少需要两个脚:接收数据输入(RX)和发送数据输出(TX)。
-
RX:接收数据串行输入。通过采样技术来区别数据和噪音,从而恢复数据。
-
TX :发送数据输出。当发送器被禁止时,输出引脚恢复到它的I/O端口配置。当发送器被激活,并且不发送数据时,TX引脚处于高电平。在单线和智能卡模式里,此I/O 口被同时用于数据的发送和接收。
板子使用串口0,接口用的232,但对于软件来说,都是一样的。
10.4串口发送(重定向printf)
10.4.1串口发送实现
下面笔者就用标准库来操作串口0。
1.串口配置
- 串口0时钟使能
串口1是挂载在 APB2 下面的外设,所以使能函数为:
rcu_periph_clock_enable(RCU_USART0);
值得注意的是,不仅要打开串口的时钟,还需要打开相应GPIO的时钟,最终的代码如下:
rcu_periph_clock_enable(RCU_GPIOA);
- 配置串口GPIO
这个比较简单,前面的章节已经讲过了,只需要注意的是,这里的GPIO不再是普通GPIO,要配置成复用功能,因此TX和RX分别配置成GPIO_MODE_AF_PP和GPIO_MODE_IN_FLOATING。
- 串口复位
当外设出现异常的时候可以通过复位设置,实现该外设的复位,然后重新配置这个外设达到让其重新工作的目的。一般在系统刚开始配置外设的时候,都会先执行复位该外设的操作。复位的是在函数usart_deinit()中完成:
void usart_deinit(uint32_t usart_periph);
比如我们要复位串口0,方法为:
usart_deinit(USART0);
- 串口参数初始化
串口初始化是以下函数设置:
void usart_baudrate_set(uint32_t usart_periph, uint32_t baudval); //设置波特率
void usart_word_length_set(uint32_t usart_periph, uint32_t wlen); //设置传输字长
void usart_stop_bit_set(uint32_t usart_periph, uint32_t stblen); //设置停止位
void usart_parity_config(uint32_t usart_periph, uint32_t paritycfg); //设置校验位
void usart_hardware_flow_rts_config(uint32_t usart_periph, uint32_t rtsconfig); //设置RTS流控
void usart_hardware_flow_cts_config(uint32_t usart_periph, uint32_t ctsconfig); //设置CTS流控
void usart_receive_config(uint32_t usart_periph, uint32_t rxconfig); //设置接收使能
void usart_transmit_config(uint32_t usart_periph, uint32_t txconfig); //设置发送使能
从上面的初始化格式可以看出初始化需要设置的参数为:波特率,字长,停止位,奇偶校验位,硬件数据流控制,模式(收,发)。 我们可以根据需要设置这些参数。
- 串口使能
串口使能是通过函数usart_enable()来实现的,这个很容易理解,使用方法是:
usart_enable(USART0);
到此,串口初始化的基本配置就算完成了,完整初始化代码如下:
/*
brief configure COM port
param[in] com: COM on the board
arg COM1: COM1 on the board
param[out] none
retval none
*/
void com_init(uint32_t com)
{
uint32_t com_id = 0U;
if(COM1 == com)
{
com_id = 0U;
}
/* enable GPIO clock */
rcu_periph_clock_enable(COM_GPIO_CLK[com_id]);
/* enable USART clock */
rcu_periph_clock_enable(COM_CLK[com_id]);
/* connect port to USARTx_Tx */
gpio_init(COM_GPIO_PORT[com_id], GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, COM_TX_PIN[com_id]);
/* connect port to USARTx_Rx */
gpio_init(COM_GPIO_PORT[com_id], GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, COM_RX_PIN[com_id]);
/* USART configure */
usart_deinit(com);
usart_baudrate_set(com, 115200U);
usart_word_length_set(com, USART_WL_8BIT);
usart_stop_bit_set(com, USART_STB_1BIT);
usart_parity_config(com, USART_PM_NONE);
usart_hardware_flow_rts_config(com, USART_RTS_DISABLE);
usart_hardware_flow_cts_config(com, USART_CTS_DISABLE);
usart_receive_config(com, USART_RECEIVE_ENABLE);
usart_transmit_config(com, USART_TRANSMIT_ENABLE);
usart_enable(com);
}
2.数据发送与接收
GD32 的发送与接收是通过数据寄存器USART_DATA来实现的,这是一个双寄存器。当向该寄存器写数据的时候,串口就会自动发送,当收到数据的时候,也是存在该寄存器内。
GD32库函数操作USART_DATA寄存器发送数据的函数是:
void usart_data_transmit(uint32_t usart_periph, uint16_t data);
通过该函数向串口寄存器 USART_DR 写入一个数据。
GD32库函数操作USART_DATA寄存器读取串口接收到的数据的函数是:
uint16_t usart_data_receive(uint32_t usart_periph);
通过该函数可以读取串口接受到的数据。
3.串口状态
串口的状态可以通过状态寄存器USART_STAT0读取。
状态寄存器的其他位我们这里就不做过多讲解,大家需要可以查看中文参考手册。
在我们固件库函数里面,读取串口状态的函数是:
FlagStatus usart_flag_get(uint32_t usart_periph, usart_flag_enum flag);
这个函数的第二个入口参数非常关键, 它是标示我们要查看串口的哪种状态, 比如上面讲解的TBE(读数据寄存器非空)以及 TC(发送完成)。例如我们要判断读寄存器是否非空(TBE), 操作库函数的方法是:
usart_flag_get (USART0, USART_FLAG_TBE);
我们要判断发送是否完成(TC),操作库函数的方法是:
usart_flag_get (USART0, USART_FLAG_TC);
这些标识号是通过枚举类型定义的:
/* USART flags */
typedef enum {
/* flags in STAT0 register */
USART_FLAG_CTSF = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 9U), /*!< CTS change flag */
USART_FLAG_LBDF = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 8U), /*!< LIN break detected flag */
USART_FLAG_TBE = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 7U), /*!< transmit data buffer empty */
USART_FLAG_TC = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 6U), /*!< transmission complete */
USART_FLAG_RBNE = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 5U), /*!< read data buffer not empty */
USART_FLAG_IDLEF = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 4U), /*!< IDLE frame detected flag */
USART_FLAG_ORERR = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 3U), /*!< overrun error */
USART_FLAG_NERR = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 2U), /*!< noise error flag */
USART_FLAG_FERR = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 1U), /*!< frame error flag */
USART_FLAG_PERR = USART_REGIDX_BIT(USART_STAT0_REG_OFFSET, 0U), /*!< parity error flag */
/* flags in STAT1 register */
USART_FLAG_BSY = USART_REGIDX_BIT(USART_STAT1_REG_OFFSET, 16U), /*!< busy flag */
USART_FLAG_EB = USART_REGIDX_BIT(USART_STAT1_REG_OFFSET, 12U), /*!< end of block flag */
USART_FLAG_RT = USART_REGIDX_BIT(USART_STAT1_REG_OFFSET, 11U) /*!< receiver timeout flag */
} usart_flag_enum;
另外,笔者在此给出输出格式的说明,请读者朋友参考。
格式 | 说明 |
---|---|
%d | 按照十进制整型数打印 |
%6d | 按照十进制整型数打印,至少6个字符宽 |
%f | 按照浮点数打印 |
%6f | 按照浮点数打印,至少6个字符宽 |
%.2f | 按照浮点数打印,小数点后有2位小数 |
%6.2f | 按照浮点数打印,至少6个字符宽,小数点后有2位小数 |
%x | 按照十六进制打印 |
%c | 打印字符 |
%s | 打印字符串 |
接下来就可以实现串口的发送了,这里对发送函数进行封装。
/**
* @brief 串口发送一个字节数据
* @param ch:待发送字符
* @retval None
*/
void usart_send_byte(uint8_t ch)
{
/* 发送一个字节数据到USART0 */
usart_data_transmit(USART0,ch);
/* 等待发送完毕 */
while (usart_flag_get(USART0, USART_FLAG_TBE) == RESET);
}
/**
* @brief 串口发送指定长度的字符串
* @param str:待发送字符串缓冲器
* strlen:指定字符串长度
* @retval None
*/
void usart_sendStr_length(uint8_t *str,uint32_t strlen)
{
unsigned int k=0;
do
{
usart_send_byte(*(str + k));
k++;
} while(k < strlen);
}
/**
* @brief 串口发送字符串,直到遇到字符串结束符
* @param str:待发送字符串缓冲器
* @retval None
*/
void usart_send_string(uint8_t *str)
{
unsigned int k=0;
do
{
usart_send_byte(*(str + k));
k++;
} while(*(str + k)!='\0');
}
这样就方便多了,然后再主函数中调用发送函数。
/*
brief main function
param[in] none
param[out] none
retval none
*/
int main(void)
{
char str[20];
//systick init
sysTick_init();
//usart init 115200 8-N-1
com_init(COM1);
usart_send_string((uint8_t*)"This is COM1\r\n");
/* sprintf函数把格式化的数据写入某个字符串 */
sprintf(str,"20%02d-%02d-%02d",22,05,15);
usart_send_string((uint8_t*)str);
while(1)
{
}
}
下面笔者还要介绍一种常用的串口打印方式I/O重定向,也就是使用printf打印数据到终端,但是我们的裸机系统没有终端,因此如果想让printf / scanf向USART0发送、获取数据,需要通过代码指定C标准库输入/输出函数的控制终端设备,也就是使用功能I/O重定向。
在stdio.h有相应的接口。
/*
* dynamically allocates a buffer of the right size for the
* formatted string, and returns it in (*strp). Formal return value
* is the same as any other printf variant, except that it returns
* -1 if the buffer could not be allocated.
*
* (The functions with __ARM_ prefixed names are identical to the
* ones without, but are available in all compilation modes without
* violating user namespace.)
*/
extern _ARMABI int fgetc(FILE * /*stream*/) __attribute__((__nonnull__(1)));
/*
* reads at most one less than the number of characters specified by n from
* the stream pointed to by stream into the array pointed to by s. No
* additional characters are read after a new-line character (which is
* retained) or after end-of-file. A null character is written immediately
* after the last character read into the array.
* Returns: s if successful. If end-of-file is encountered and no characters
* have been read into the array, the contents of the array remain
* unchanged and a null pointer is returned. If a read error occurs
* during the operation, the array contents are indeterminate and a
* null pointer is returned.
*/
extern _ARMABI int fputc(int /*c*/, FILE * /*stream*/) __attribute__((__nonnull__(2)));
下面我们以实现printf打印数据到USART(即重定义fputc函数)的实现过程。
/**
* @brief 重定向c库函数printf到USART1
* @param None
* @retval
*/
int fputc(int ch, FILE *f)
{
/*清除标志位*/
usart_flag_clear(USART0,USART_FLAG_TC);
/* 发送一个字节数据到USART0 */
usart_data_transmit(USART0, (uint8_t) ch);
/* 等待发送完毕 */
while (usart_flag_get(USART0, USART_FLAG_TC) == RESET);
return (ch);
}
scanf同理。
/**
* @brief 重定向c库函数scanf到USART0
* @param None
* @retval None
*/
int fgetc(FILE *f)
{
/* 等待串口0输入数据 */
while (usart_flag_get(USART0, USART_FLAG_RBNE) == RESET);
return (int)usart_data_receive(USART0);
}
接下来就可使用printf和scanf函数了。
/*
brief main function
param[in] none
param[out] none
retval none
*/
int main(void)
{
char str[20];
//systick init
sysTick_init();
//usart init 115200 8-N-1
com_init(COM1);
printf("This is COM1\r\n");
/* sprintf函数把格式化的数据写入某个字符串 */
sprintf(str,"20%02d-%02d-%02d",22,05,15);
printf("%s",str);
while(1)
{
}
}
完整代码请查看配套程序,另外还需添加微库以便支持printf。具体设置参看本节后文的小贴士部分。
我们来总结下串口发送的流程:
1.初始化硬件,时钟;
2.USART 的GPIO初始化,USART参数初始化;
3.重定向printf
4.打印输出
10.4.2实验现象
将程序编译好下载到板子中,打开串口助手,按下图设置相应参数,按下板子的复位按键,在接收区可以看到如下信息。
10.5串口接收数据(中断方式)
10.5.1串口接收实现
中断方式相对于与普通方式,还需要开启中断并且初始化 NVIC以及中断服务函数。
- 开启中断
在接收到数据的时候(RBNE读数据寄存器非空),我们要产生中断,那么我们开启中断的方法是:
usart_interrupt_enable(USART0, USART_INT_RBNE); /* 使能串口0接收中断 */
在发送数据结束的时候( TC, 发送完成) 要产生中断,那么方法是:
usart_interrupt_enable(USART0, USART_INT_TBE);
开启NVIC中断以及优先级。
nvic_irq_enable(USART0_IRQn, 0, 0);
- 中断服务函数
/*!
\brief this function handles USART0 exception
\param[in] none
\param[out] none
\retval none
*/
void USART0_IRQHandler(void)
{
uint8_t ch;
if(RESET != usart_interrupt_flag_get(USART0, USART_INT_FLAG_RBNE)) {
/* read one byte from the receive data register */
ch = (uint8_t)usart_data_receive(USART0);
printf( "%c", ch ); //将接受到的数据直接返回打印
}
}
在中断服务程序中,接收到数据后立即输出。
主函数代码如下:
/*
brief main function
param[in] none
param[out] none
retval none
*/
int main(void)
{
char str[20];
//systick init
sysTick_init();
//usart init 115200 8-N-1
com_init(COM1);
printf("This is COM1\r\n");
/* sprintf函数把格式化的数据写入某个字符串 */
sprintf(str,"20%02d-%02d-%02d",22,05,15);
printf("%s\r\n",str);
while(1)
{
}
}
总结下串口接收的编程流程:
1.硬件初始化,时钟初始化;
2.串口GPIO初始化,串口参数配置;
3.在main()函数中使能中断接收;
4.编写中断回调函数,处理接收的数据,
【注】中断接收函数只能触发一次接收中断,所以我们需要在中断回调函数中再次调用中断接收函数。这里可以对比下标准库的流程。
10.5.2实验现象
将程序编译好下载到板子中,打开串口助手,按下图设置相应参数,按下板子的复位按键,在接收区可以看到如下信息。
小贴士:
printf 函数重定向
要想 printf() 函数工作的话,我们需要把 printf() 重新定向到串口中。重定向是指用户可以自己重写 C 的库函数,当连接器检查到用户编写了与 C 库函数相同名字的函数时,优先采用用户编写的函数,这样用户就可以实现对库的修改了。
为了实现重定向 printf() 函数,我们需要重写 fputc() 这个 C 标准库函数,因为 printf()在 C 标准库函数中实质是一个宏,最终是调用了 fputc() 这个函数。
重定向的这部分工作,由gd32f2_usart.c 文件中的 fputc(int ch, FILE *f)这个函数来完成。重定向时,我们把 fputc( ) 的形参 ch,作为串口将要发送的数据,也就是说,当使用 printf( ) 时,它先调用这个 fputc( ) 函数,然后使用GD库的串口发送函数 usart_data_transmit (),把数据转移到发送数据寄存器 TDR,触发我们的串口向 PC 发送一个相应的数据。调用完 usart_data_transmit( )后,要使用
while (usart_flag_get(USART0, USART_FLAG_TC) == RESET)
语句不停地检查串口发送是否完成的标志位TC,一直检测到标志为“完成”,才进入下一步的操作,避免出错。在这段 while 循环检测的延时中,串口外设已经由发送控制器以及根 据我们的配置把数据从移位寄存器一位一位地通过串口线 Tx 发送出去了。
【注意】printf函数在“stdio.h”头文件里,使用该函数必须引用“stdio.h”库, 还
要在编译器中设置一个选项 Use MicroLIB (使用微库)。设置方式如下:
单击Project,选择option选项,再选择Target 勾选Use MicroLIB 即可。
欢迎访问我的网站
BruceOu的哔哩哔哩
BruceOu的主页
BruceOu的博客
BruceOu的CSDN博客
BruceOu的简书
BruceOu的知乎
资源获取方式
1.关注公众号[嵌入式实验楼]
2.在公众号回复关键词[GD32开发实战指南]获取资料提取码
学到了